/**
 * Licensed under the MIT License
 *
 * Copyright (c) 2010 Alexandre Croiseaux
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 *
 */
package marcel.display.gui.form
{
	import flash.display.DisplayObject;
	import flash.display.DisplayObjectContainer;
	import flash.events.Event;
	import flash.events.KeyboardEvent;
	import flash.geom.Rectangle;
	import flash.net.URLVariables;
	import flash.ui.Keyboard;
	import marcel.display.gui.components.ComboBox;
	import marcel.display.gui.components.Label;
	import marcel.display.View;
	import marcel.external.browser.WModeManager;
	import marcel.utils.StringUtils;
	
	/**
	 * Form class used to display a form
	 * @author Alexandre Croiseaux
	 */
	public class Form
	{
		//--------------------------------------------------------------------------
		//
		//  Private vars
		//
		//--------------------------------------------------------------------------
		private var _container:DisplayObjectContainer;
		private var _currentContainer:DisplayObjectContainer;
		private var _errorsTexts:XMLList;
		private var _useBadges:Boolean;
		private var _errors:/*String*/Array;
		private var _items:/*FormItem*/Array;
		private var _boxes:/*View*/Array;
		private var _useHighlight:Boolean;
		private var _tooltip:FormErrorToolTip;
		private var _enterCallback:Function;
		
		
		//--------------------------------------------------------------------------
		//
		//  Constructor
		//
		//--------------------------------------------------------------------------
		/**
		 * Constructe a new Form instance
		 * @param	container	The DisplayObjectContainer instance that will handle gui elements
		 * @param	errorsTexts	The list of xml nodes used to get localized error texts
		 * @param	useHighlight Indicates if the form must automatically highlight componants on form errors by calling the IFormComponent.displayError method.
		 * @param	useBadges	Indicates if the form must automatically show a badge and a tooltip on form errors
		 */
		public function Form(container:DisplayObjectContainer, errorsTexts:XMLList, useHighlight:Boolean = true, useBadges:Boolean = false)
		{
			_container = container;
			_currentContainer = _container;
			_errorsTexts = errorsTexts;
			_useBadges = useBadges;
			_useHighlight = useHighlight;
			_items = [];
			_boxes = [];
		}
		
		
		//--------------------------------------------------------------------------
		//
		//  Public methods
		//
		//--------------------------------------------------------------------------
		/**
		 * Parses and build the form
		 * @param	xml	The xml data to build the form from
		 */
		public function parseXML(xml:XMLList):void
		{
			// go for recursive
			parseFormNode(xml[0]);
		}
		
		/**
		 * Returns an item of the current Form with its name
		 * @param	name	the name attribute used in the form XML
		 * @return	A FormItem instance, or null if not found
		 */
		public function getItem(name:String):FormItem
		{
			for (var i:int = 0; i < _items.length; i++)
			{
				if (_items[i].name == name) return _items[i];
			}
			return null;
		}
		
		/**
		 * Returns all the items of the current Form
		 * @return	A Array of FormItem instances
		 */
		public function getItems():/*FormItem*/Array
		{
			return _items;
		}
		
		/**
		 * Returns the values of each elements of the Form as en URLVariable instance that can be used directly to send data to the server
		 * @return	A URLVariables instance, with key/value data
		 */
		public function getValues():URLVariables
		{
			var v:URLVariables = new URLVariables();
			for each (var item:FormItem in _items)
			{
				if (item.name && !(item.component is Label)) v[item.name] = item.getValue();
			}
			return v;
		}
		
		/**
		 * Set the values for each element of the form
		 * @param	keyValues	A URLVariables instance, with key/value data (key must match the name of the form item)
		 */
		public function setValues(keyValues:URLVariables):void
		{
			var item:FormItem;
			for each (var key:String in keyValues)
			{
				item = getItem(key);
				if (item) item.setValue(keyValues[key]);
			}
		}
		
		/**
		 * Returns an Array of strings containing the current errors messages of the form
		 * @return An Array of strings
		 */
		public function getErrors():/*String*/Array
		{
			return _errors;
		}
		
		/**
		 * Returns an Array of unvalid FormItem
		 * @return An Array of FormItem
		 */
		public function getErrorItems():/*FormItem*/Array
		{
			var aError:Array = [];
			for each(var item:FormItem in _items)
			{
				if (!item.isValid()) aError.push(item);
			}
			
			return aError;
		}
		
		/**
		 * Validate the current form, displaying badges on errors if enabled, and filling values and errors data.
		 * @return	true if the form is valid (no error found), false otherwise
		 */
		public function validate():Boolean
		{
			var valid:Boolean = true;
			_errors = [];
			for (var i:int = 0; i < _items.length; i++)
			{
				var ok:Boolean = _items[i].isValid();
				if (!ok)
				{
					valid = false;
					var str:String = _items[i].getErrorMessage();
					_errors.push(str);
				}
			}
			WModeManager.getInstance().callback = null;// clear wmode singleton reference
			
			return valid;
		}
		
		/**
		 * Return the hbox or vbox instance used in the form with the specifed name
		 * @param	name	the name of the box to return
		 * @return	A View instance
		 */
		public function getBoxByName(name:String):View
		{
			for (var i:int = 0; i < _boxes.length; i++)
			{
				if (_boxes[i] && _boxes[i].name == name) return _boxes[i];
			}
			return null;
		}
		
		
		/**
		 * Activates the ENTER key to submit the form. The form displayobject container passed to the constructor must be added to stage before calling this method.
		 * @param	callback	The callback function to call when the enter key is pressed. No arguments are passed. Pass callback = null to disable Enter key.
		 */
		public function enableEnterKey(callback:Function = null):void
		{
			if (_container && _container.stage)
			{
				if (callback != null)
				{
					_container.stage.addEventListener(KeyboardEvent.KEY_DOWN, onKeyDown);
					_container.addEventListener(Event.REMOVED, onRemoved);
				}
				else
				{
					_container.stage.removeEventListener(KeyboardEvent.KEY_DOWN, onKeyDown);
					_container.removeEventListener(Event.REMOVED, onRemoved);
				}
				
				_enterCallback = callback;
			}
		}
		
		
		
		//--------------------------------------------------------------------------
		//
		//  Private methods
		//
		//--------------------------------------------------------------------------
		private function parseFormNode(xml:XML):void
		{
			// parse nodes
			for each (var node:XML in xml.children())
			{
				switch (node.name().toString())
				{
					case "hbox":
					case "vbox":
						var orient:String = node.name().toString() == "hbox" ? View.ORIENTATION_HORIZONTAL : View.ORIENTATION_VERTICAL;
						var view:View = new View(0, 0, orient);
						_currentContainer = _currentContainer.addChild(view) as View;
						for each (var attr:XML in node.attributes())
						{
							try	{ view[attr.name().toString()] = attr.toString(); } catch (e:Error) { };
						}
						_boxes.push(view);
						parseFormNode(node);
						break;
					case "item":
						var item:FormItem = new FormItem(this, _useHighlight, _useBadges);
						item.parseXML(node);
						if (item.component is DisplayObject) _currentContainer.addChild(item.component as DisplayObject);
						_items.push(item);
						break;
				}
			}
			
			// bring comboboxes to front
			var i:uint = _currentContainer.numChildren;
			while (--i >= 0)
			{
				if (_currentContainer.getChildAt(i) is ComboBox) _currentContainer.setChildIndex(_currentContainer.getChildAt(i), _currentContainer.numChildren - 1);
				else if (_currentContainer.getChildAt(i) is View) _currentContainer.setChildIndex(_currentContainer.getChildAt(i), _currentContainer.numChildren - 1);
			}
			
			// go 1 step up in displaylist hierarchy
			if (_currentContainer != _container) _currentContainer = _currentContainer.parent;
		}
		
		/**
		 * @private
		 */
		internal function getErrorText(errorId:uint, params:Array):String
		{
			return StringUtils.injectVars(_errorsTexts.error.(@id == errorId), params);
		}
		
		/**
		 * @private
		 */
		internal function openToolTip(rollTarget:DisplayObject, msg:String):void
		{
			if (_tooltip == null)
			{
				_tooltip = new FormErrorToolTip();
				_container.addChild(_tooltip);
			}
			var rect:Rectangle = rollTarget.getRect(_container);
			_tooltip.open(msg, rect.x, rect.y);
		}
		
		/**
		 * @private
		 */
		internal function closeToolTip():void
		{
			if (_tooltip) _tooltip.close();
		}
		
		//--------------------------------------------------------------------------
		//
		//  Events
		//
		//--------------------------------------------------------------------------
		private function onRemoved(event:Event):void
		{
			enableEnterKey(null);
		}
		
		private function onKeyDown(event:KeyboardEvent):void
		{
			if (event.keyCode == Keyboard.ENTER && _enterCallback != null) _enterCallback();
		}
	}

}